列表中非常常見的搜尋列
如下圖所示



搜尋列的組成大約分幾種:
input 輸入編號或是帳號。select 下拉式選單。date 時間工具。-src
  |-app
    |-cms
        |-...
    |-modules
       |-search
            |-search-input
                |-search-input.component.css
                |-search-input.component.html
                |-search-input.component.ts
            |-search-select
                |-search-select.component.css
                |-search-select.component.html
                |-search-select.component.ts
            |-search-date
                |-search-date.component.css
                |-search-date.component.html
                |-search-date.component.ts
            |-search.module.ts
            |-search.ts
其實後台最多的就是列表,而列表中通常都有資料篩選的選項。
那非常多的Component上都會用到,
筆者就會建議把能夠複用的都包起來,
要用到的時候只要給設定值就好。
資料篩選的選項,
目前分成三種 input、select、date,
也將會有三種子組件相對應,
以及把這三種子組件包起來成SearchModule,
來做搜尋的各種設定。
search.module.ts:請先安裝
ng-pick-datetime、
ng-pick-datetime-moment、
moment。
Demo所使用的時間套件為
ng-pick-datetime相關,
因 Angular Materia 的時間套件只能年月日而已。
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared/shared.module";
import {
  DateTimeAdapter,
  OWL_DATE_TIME_FORMATS,
  OWL_DATE_TIME_LOCALE,
  OwlDateTimeModule,
  OwlNativeDateTimeModule
} from "ng-pick-datetime";
import { MomentDateTimeAdapter, OWL_MOMENT_DATE_TIME_FORMATS } from "ng-pick-datetime-moment";
import { SearchSelectComponent } from "./search-select/search-select.component";
import { SearchInputComponent } from "./search-input/search-input.component";
import { SearchDateComponent } from "./search-date/search-date.component";
@NgModule({
  providers: [
    {
      provide: DateTimeAdapter,
      useClass: MomentDateTimeAdapter,
      deps: [OWL_DATE_TIME_LOCALE]
    },
    { 
      provide: OWL_DATE_TIME_FORMATS, 
      useValue: OWL_MOMENT_DATE_TIME_FORMATS 
    }
  ],
  imports: [SharedModule, OwlDateTimeModule, OwlNativeDateTimeModule],
  declarations: [
    SearchSelectComponent, 
    SearchInputComponent, 
    SearchDateComponent
  ],
  exports: [
    OwlDateTimeModule,
    OwlNativeDateTimeModule,
    SearchSelectComponent,
    SearchInputComponent,
    SearchDateComponent
  ]
})
export class SearchModule {}
那接下來我們就要開始設定Search物件裡面的屬性。
以下面的圖來看


大概會有以下的搜尋條件
| 項目 | 變數名稱 | 型別 | 
|---|---|---|
| 會員編號 | id | number | 
| 會員等級 | levelId | number | 
| 會員名稱 | name | string | 
| 商品類型 | typeId | number | 
| 商品狀態 | status | number | 
| 訂單狀態 | status | number | 
| 訂單開始日期 | start | Moment | 
| 訂單結束日期 | end | Moment | 
--
接下來還會有幾種狀況:
舉例來說:
如果要搜尋某個會員的 登入紀錄,
那麼進入 登入紀錄 時的Component就會帶入會員編號id。
但如果是在畫面上有個搜尋input要輸入會員編號id,
這時候會有型別的差異,
在html的輸入任何值都是string。
所以會放在html做搜尋的項目,
只要是number屬性的,建議會再做一層:
| 項目 | 變數名稱 | 型別 | 
|---|---|---|
| 會員編號 | id | number | 
會員編號 | 
idSel | 
string | 
| 會員等級 | levelId | number | 
會員等級 | 
levelIdSel | 
string | 
| 會員名稱 | name | string | 
| 商品類型 | typeId | number | 
商品類型 | 
typeIdSel | 
string  | 
| 商品狀態 | status | number | 
| 訂單狀態 | status | number | 
| 訂單開始日期 | start | Moment | 
| 訂單結束日期 | end | Moment | 
--
雖然有會員狀態、管理者狀態、商品狀態、訂單狀態,
都是各個不同的Model,
但因為統一做Search,在這裡就跟各個Model無關,
也就是說變數名稱相同就統一起來。
| 項目 | 變數名稱 | 型別 | 
|---|---|---|
| 會員編號 | id | number | 
會員編號 | 
idSel | 
string | 
| 會員等級 | levelId | number | 
會員等級 | 
levelIdSel | 
string | 
| 會員名稱 | name | string | 
| 商品類型 | typeId | number | 
商品類型 | 
typeIdSel | 
string  | 
| 狀態 | status | number | 
| 狀態 | statusSel | string | 
| 訂單開始日期 | start | Moment | 
| 訂單結束日期 | end | Moment | 
--
通常一個Search物件,
我們會把寫入的條件賦值給Search物件的屬性。
let s = new Search();
setStatus(idSel:string){
    s.idSel = idSel;
}
然後使用者把所有的搜尋條件都賦值後,
我們要把所有被賦值的變數一一過濾以及轉型。
getSearch(){
    let obj = <Search>{};
    if(!!s.idSel){
        obj.id = +s.idSel
    }
    ...
    return obj;
}
為什麼要過濾跟轉型?
因為要送去後端的參數不一定跟Search物件的屬性相同名稱。
舉例來說,json-server 的判斷條件:
_gte : 大于等于
_lte : 小于等于
_ne : 不等于
_like : 包含
於是我們要搜尋日期區間:
http://localhost:3000/orders/?inserted_gte=1569859200000&&inserted_lte=1572537600000
inserted在Model建置為 建立時間。
Model請參照day06 json-server 模擬與 Model 建置(二)。
所以在過濾取值的時候應該這麼做:
getSearch(s:Search){
    let obj = <Search>{};
    if(!!s.start && !!s.end){
        obj["inserted_gte"] = s.start.valueOf()
        obj["inserted_lte"] = s.end.valueOf()
    }
    ...
    return obj;
}
start、end為 Moment 型別,要轉換為時間戳需使用valueOf()。
--
在列表中搜尋時如果後端沒有處理,
要前端自己去關聯其他資料表,
這時候參考json-server的關聯條件:
向上關聯:(單個)
GET /comments/1?_expand=post
向下關聯:(多個)
GET /posts?_embed=comments
注意複數s,上面範例有兩個資料表
posts、comments
參考至 https://www.cnblogs.com/fly_dragon/p/9150732.html
所以我們可以在Search物件裡這麼做:
getSearch(s:Search){
    let obj = <Search>{};
    if (!!this._expand) {
      obj["_expand"] = s.expand;
    }
    if (!!this._embed) {
      obj["_embed"] = s._embed;
    }
    ...
    return obj;
}
search.tsimport * as _moment from "moment";
import { Moment } from "moment";
const moment = (_moment as any).default ? (_moment as any).default : _moment;
class Base {
  public static getGetters(): string[] {
    return Object.keys(this.prototype).filter(name => {
      return typeof Object.getOwnPropertyDescriptor(this.prototype, name)["get"] === "function";
    });
  }
  public static getSetters(): string[] {
    return Object.keys(this.prototype).filter(name => {
      return typeof Object.getOwnPropertyDescriptor(this.prototype, name)["set"] === "function";
    });
  }
}
export interface IValid {
  type: string;
  valid: boolean;
}
export class Search extends Base {
  private validObjs: IValid[] = [];
  constructor(
    private _id: number = 0,
    private _idSel: string = "",
    private _levelId: number = 0,
    private _levelIdSel: string = "",
    private _typeId: number = 0,
    private _typeIdSel: string = "",
    private _name: string = "",
    private _status: number = 0,
    private _statusSel: string = "",
    private _start: Moment = null,
    private _end: Moment = null,
    private _expand:string = "",
    private _embed:string = "",
    private _check: boolean = true
  ) {
    super();
  }
  set id(_id) {
    this._id = _id;
  }
  set idSel(_idSel) {
    this._idSel = _idSel;
  }
  set levelId(_levelId) {
    this._levelId = _levelId;
  }
  set levelIdSel(_levelIdSel) {
    this._levelIdSel = _levelIdSel;
  }
  set typeId(_typeId) {
    this._typeId = _typeId;
  }
  set typeIdSel(_typeIdSel) {
    this._typeIdSel = _typeIdSel;
  }
  set name(_name) {
    this._name = _name;
  }
  set status(_status) {
    this._status = _status;
  }
  set statusSel(_statusSel) {
    this._statusSel = _statusSel;
  }
  set start(_start) {
    this._start = _start;
  }
  set end(_end) {
    this._end = _end;
  }
  set expand(_expand) {
    this._expand = _expand;
  }
  set embed(_embed) {
    this._embed = _embed;
  }
  set check(_check) {
    this._check = _check;
  }
  /**GET */
  get id(): number {
    return this._id;
  }
  get idSel(): string {
    return this._idSel;
  }
  get levelId(): number {
    return this._levelId;
  }
  get levelIdSel(): string {
    return this._levelIdSel;
  }
  get typeId(): number {
    return this._typeId;
  }
  get typeIdSel(): string {
    return this._typeIdSel;
  }
  get name(): string {
    return this._name;
  }
  get status(): number {
    return this._status;
  }
  get statusSel(): string {
    return this._statusSel;
  }
  get check(): boolean {
    return this._check;
  }
  get start(): Moment {
    return this._start;
  }
  get end(): Moment {
    return this._end;
  }
  get expand(): string {
    return this._expand;
  }
  get embed(): string {
    return this._embed;
  }
  setValidObjs(obj: IValid) {
    let index = this.validObjs
      .map((validObj: IValid) => {
        return validObj.type;
      })
      .indexOf(obj.type);
    if (index === -1) {
      this.validObjs.push(obj);
    } else {
      this.validObjs[index] = obj;
    }
    this.setCheck();
  }
  setCheck() {
    this._check = true;
    this.validObjs.forEach((validObj: IValid) => {
      if (!validObj.valid) {
        this._check = false;
        return;
      }
    });
  }
  getSearch(): Search {    //過濾以及轉型
    let obj = <Search>{};
    if (!!this._id) {
      obj["id"] = this._id;
    }
    if (!!this._idSel) {
      obj["id"] = +this._idSel;
    }
    if (!!this._levelId) {
      obj["levelId"] = this._levelId;
    }
    if (!!this._levelIdSel) {
      obj["levelId"] = +this._levelIdSel;
    }
    if (!!this._typeId) {
      obj["typeId"] = this._typeId;
    }
    if (!!this._typeIdSel) {
      obj["typeId"] = +this._typeIdSel;
    }
    if (!!this._name) {
      obj["name"] = this._name;
    }
    if (!!this._status) {
      obj["status"] = this._status;
    }
    if (!!this._statusSel) {
      obj["status"] = +this._statusSel;
    }
    if (!!this._start) {
      obj["inserted_gte"] = this._start.valueOf();
    }
    if (!!this._end) {
      obj["inserted_lte"] = this._end.valueOf();
    }
    if (!!this._expand) {
      obj["_expand"] = this._expand;
    }
    if (!!this._embed) {
      obj["_embed"] = this._embed;
    }
    return obj;
  }
  setSearchDate(indexDateTab: string) {
    switch (indexDateTab) {
      case "now":
        this._start = moment().subtract(1, "hours");
        this._end = moment()
          .startOf("date")
          .add(1, "days");
        break;
      case "today":
        this._start = moment().startOf("date");
        this._end = moment()
          .startOf("date")
          .add(1, "days");
        break;
      case "yesterday":
        this._start = moment()
          .startOf("date")
          .subtract(1, "days");
        this._end = moment().startOf("date");
        break;
      case "month":
        this._start = moment().startOf("month");
        this._end = moment()
          .startOf("month")
          .add(1, "months");
        break;
    }
  }
}
大概可以分為幾個部分
get、set關鍵字,想了解更多請參閱
ES6的JavaScript。
一個列表內可能有多種搜尋項目,
每個搜尋項目的驗證是獨立的,
但不是每一個搜尋項目都會是有效的。
private validObjs: IValid[] = [];
validObjs 就是裝同一列表中所擁有的搜尋項目的有效值。
如下圖
需要驗證
等級(levelId)
會員編號(id)
會員名稱(name)
如果 等級 有效,則:
let o:IValid = {
    type: 'levelId';
    valid: true;
}
setValidObjs(o)
setValidObjs()就是把每個 IValid 物件裝進validObjs裡。setValidObjs()會先判斷 IValid 物件是否已存在(以type區分)。
以上述例子來說,不管使用者怎麼輸入條件,
最終validObjs結果只會有三個:
console.log(validObjs)
/*
[
    {type: 'levelId', valid: true},
    {type: 'id', valid: false},
    {type: 'name', valid: true}
]
*/
最後就是setCheck()要來check validObjs,
只要其中有一個是false,則不能搜尋!

button-search則是判斷Search物件的
check屬性
當為 true 的時候才能按
--
最後面的setSearchDate()就是單純的日期函式,
可能會用在日期搜尋的初始值,
或是日期的快捷鍵等等。
此為完整專案範例碼,連線方式為json-server。
https://stackblitz.com/edit/ngcms-json-server
一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json到本地端,
並下指令:
json-server db.json
json-server開啟成功後請連結此網址:
https://ngcms-json-server.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690